package ru.ifmo.votinov;

import universum.bi.BeingInterface;
import universum.bi.Constants;
import universum.bi.Location;
import universum.bi.PointInfo;

import java.util.*;

class EatersColony extends ArrayList<ConquerorBeing> {
    private static final int STEPS_TO_GATHER_ENERGY = 3;
    private static final float SAVED_ENERGY = 0.6f;

    private Map<ConquerorBeing, Float> givenEnergy;
    private Map<ConquerorBeing, Float> amountToGive;
    private List<ConquerorBeing> eaters;
    private Map<ConquerorBeing, ConquerorBeing> energyGivers;
    private List<ConquerorBeing> defenders;
    private Map<ConquerorBeing, Location> defendersTargetLocations;
    private Map<ConquerorBeing, Integer> defendersTargetIds;
    private float foodAmount;
    private boolean initied;

    public EatersColony() {
        amountToGive = new HashMap<ConquerorBeing, Float>();
        givenEnergy = new HashMap<ConquerorBeing, Float>();
        eaters = new ArrayList<ConquerorBeing>();
        energyGivers = new HashMap<ConquerorBeing, ConquerorBeing>();
        defenders = new ArrayList<ConquerorBeing>();
        defendersTargetLocations = new HashMap<ConquerorBeing, Location>();
        defendersTargetIds = new HashMap<ConquerorBeing, Integer>();
    }

    boolean shouldEat(BeingInterface bi, ConquerorBeing being) {
        ensureInitied(bi);
        return eaters.contains(being);
    }

    boolean shouldGiveEnergy(BeingInterface bi, ConquerorBeing being) {
        ensureInitied(bi);
        return energyGivers.containsKey(being);
    }

    Location getAttackLocationForDefender(BeingInterface bi, ConquerorBeing being) {
        ensureInitied(bi);
        return defendersTargetLocations.get(being);
    }

    Integer getTargetEnemyId(ConquerorBeing being) {
        return defendersTargetIds.get(being);
    }

    ConquerorBeing getEnergyTaker(ConquerorBeing giver) {
        return energyGivers.get(giver);
    }

    Float getAmountToGive(ConquerorBeing being) {
        return amountToGive.get(being);
    }

    void handleNewTurnStarted() {
        initied = false;
    }

    private void ensureInitied(BeingInterface bi) {
        if (!initied) {
            init(bi);
        }
    }

    void init(BeingInterface bi) {
        if (initied) {
            return;
        }        
        eaters.clear();
        energyGivers.clear();
        givenEnergy.clear();
        amountToGive.clear();
        defendersTargetLocations.clear();
        defendersTargetIds.clear();

        if (size() == 0) {
            return;
        }
        createDefendersIfNeeded(bi);
        if (size() == 0) {
            return;
        }
        updateFoodAmount(bi);
        sortByEnergyPercent();
        if (getTurn() < getPrepareToGatherStep()) {
            if (get(size() - 1).getEnergyPercent() < SAVED_ENERGY) {
                for (ConquerorBeing being : this) {
                    if (!hasCommand(being) && being.getEnergyPercent() < SAVED_ENERGY && canEat()) {
                        addEater(being);
                    }
                }
            }
        }
        for (int i = size() - 1; i >= 0; i--) {
            ConquerorBeing being = get(i);
            if (!hasCommand(being)) {
                if (canGather(bi, being)) {
                    goToGather(being);
                } else if (canEat()) {
                    addEater(being);
                } else if (canBecomeSearcher(bi, being)) {
                    becomeSearcher(being);
                }
            }
        }
        for (int i = 0; i < size(); i++) {
            ConquerorBeing being = get(i);
            if (!hasCommand(being) && energyCanGive(being) > 0) {
                for (int j = size() - 1; j > i; j--) {
                    ConquerorBeing taker = get(j);
                    if (getEnergy(taker) >= getEnergy(being) && needEnergyToBorn(taker) > 0) {
                        giveEnergy(being, taker, Math.min(energyCanGive(being), needEnergyToBorn(taker)));
                        break;
                    }
                }
            }
        }
        if (getTurn() > CorporateMind.getInstance().getMaxTurn() - 5) {
            sortByMass();
            for (ConquerorBeing taker : this) {
                float needEnergy = needEnergyToBorn(taker);
                if (needEnergy > 0) {
                    ConquerorBeing giver = null;
                    for (ConquerorBeing being : this) {
                        if (!hasCommand(being) && !energyGivers.values().contains(being) &&
                                being.getMass() <= taker.getMass() &&
                                being.getEnergy() > needEnergy && being.getEnergy() + getEnergy(taker) <= taker.getMass() &&
                                (giver == null || giver.getMass() > being.getMass())) {
                                giver = being;
                        }
                    }
                    if (giver != null) {
                        giveEnergy(giver, taker, giver.getEnergy());
                    }
                }
            }
        }
        initied = true;
    }

    private void createDefendersIfNeeded(BeingInterface bi) {
        List<EnemyInfo> enemyInfos = getEnemyInfos(bi);
        sortByEnergyPercent();
        List<ConquerorBeing> freeDefenders = new ArrayList<ConquerorBeing>(defenders);
        defenders.clear();
        List<ConquerorBeing> beings = new ArrayList<ConquerorBeing>(this);
        for (EnemyInfo ei : enemyInfos) {
            float mass = CorporateMind.getInstance().getEnemyMass(ei.getId());
            float energyToBeat;
            if (mass != 0) {
                energyToBeat = 0.6f * bi.getMass(get(0), ei.getId());
            } else {
                energyToBeat = 0.01f;
            }
            while (energyToBeat > 0) {
                ConquerorBeing being = null;
                if (!freeDefenders.isEmpty()) {
                    being = freeDefenders.remove(freeDefenders.size() - 1);
                } else if (!beings.isEmpty()) {
                    being = beings.remove(beings.size() - 1);
                }
                if (being != null) {
                    energyToBeat -= Constants.K_fight * being.getMass();
                    addDefender(being, ei.getLocation(), ei.getId());
                } else {
                    break;
                }
            }
        }
        for (ConquerorBeing being : defenders) {
            if (defendersTargetIds.get(being) == null) {
                removeDefender(being);
            }
        }
    }

    private List<EnemyInfo> getEnemyInfos(BeingInterface bi) {
        List<EnemyInfo> enemyInfos = new ArrayList<EnemyInfo>();
        for (PointInfo pi : bi.getNeighbourInfo(get(0))) {
            Integer[] ids = pi.getEntities(get(0), null);
            for (Integer id : ids) {
                if (!bi.getOwner(get(0), id).equals(bi.getOwner(get(0), bi.getId(get(0))))) {
                    enemyInfos.add(new EnemyInfo(id, pi.getLocation()));
                }
            }
        }
        PointInfo pi = bi.getPointInfo(get(0));
        Integer[] ids = pi.getEntities(get(0), null);
        for (Integer id : ids) {
            if (!bi.getOwner(get(0), id).equals(bi.getOwner(get(0), bi.getId(get(0))))) {
                enemyInfos.add(new EnemyInfo(id, pi.getLocation()));
            }
        }
        return enemyInfos;
    }

    private void addDefender(ConquerorBeing being, Location location, Integer id) {
        defenders.add(being);
        defendersTargetIds.put(being, id);
        defendersTargetLocations.put(being, location);
        being.processEvent(ConquerorBeing.E2);
    }

    void addBeing(ConquerorBeing being) {
        add(being);
    }

    void removeBeing(ConquerorBeing being) {
        remove(being);
    }

    void removeDefender(ConquerorBeing being) {
        defenders.remove(being);
    }

    private void sortByEnergyPercent() {
        Collections.sort(this, new Comparator<ConquerorBeing>() {
            public int compare(ConquerorBeing o1, ConquerorBeing o2) {
                return Float.compare(o1.getEnergyPercent(), o2.getEnergyPercent());
            }
        });
    }

    private void sortByMass() {
        Collections.sort(this, new Comparator<ConquerorBeing>() {
            public int compare(ConquerorBeing o1, ConquerorBeing o2) {
                return Float.compare(o1.getMass(), o2.getMass());
            }
        });
    }

    private boolean hasCommand(ConquerorBeing being) {
        return energyGivers.containsKey(being) || eaters.contains(being) || defenders.contains(being);
    }

    private void addEater(ConquerorBeing being) {
        foodAmount -= Math.min(Constants.K_bite * being.getMass(), being.getMass() - getEnergy(being));
        eaters.add(being);
    }

    private void becomeSearcher(ConquerorBeing being) {
        being.processEvent(ConquerorBeing.E3);
    }

    private void goToGather(ConquerorBeing being) {
        being.processEvent(ConquerorBeing.E5);
    }

    private float getEnergy(ConquerorBeing being) {
        return being.getEnergy() + (givenEnergy.get(being) != null ? givenEnergy.get(being) : 0);
    }

    private void updateFoodAmount(BeingInterface bi) {
        foodAmount = bi.getPointInfo(get(0)).getCount(get(0));
    }

    private boolean canEat() {
        return foodAmount > 0;
    }

    private boolean canBecomeSearcher(BeingInterface bi, ConquerorBeing being) {
        return CorporateMind.getInstance().canSearchFromLocation(bi, being, bi.getLocation(being));
    }

    private float energyCanGive(ConquerorBeing being) {
        if (getTurn() < getPrepareToGatherStep()) {
            return 0;
        } else {
            return getEnergy(being) - (Constants.K_emin +
                    ((float)CorporateMind.getInstance().getMaxTurn() - getTurn()) * Constants.K_masscost) * being.getMass();
        }
    }

    private float needEnergyToBorn(ConquerorBeing being) {
        if (getTurn() < getStepToGather()) {
            return being.getMass() - getEnergy(being) - (givenEnergy.get(being) != null ? givenEnergy.get(being) : 0);
        } else {
            return (Constants.K_toborn + Constants.K_masscost) * being.getMass() - getEnergy(being) - (givenEnergy.get(being) != null ? givenEnergy.get(being) : 0);
        }
    }

    private void giveEnergy(ConquerorBeing giver, ConquerorBeing taker, float amount) {
        amountToGive.put(giver, amount);
        givenEnergy.put(taker, amount + (givenEnergy.get(taker) != null ? givenEnergy.get(taker) : 0));
        givenEnergy.put(giver, -amount + (givenEnergy.get(giver) != null ? givenEnergy.get(giver) : 0));
        energyGivers.put(giver, taker);
    }

    private boolean canGather(BeingInterface bi, ConquerorBeing being) {
        if (getTurn() < getStepToGather()) {
            return false;
        } else {
            Location gatheringLocation = CorporateMind.getInstance().getGatheringLocation(bi, being);
            return !gatheringLocation.equals(bi.getLocation(being)) &&
                energyCanGive(being) > SearchUtil.moveCost(bi.getLocation(being), gatheringLocation, being) &&
                SearchUtil.movesCount(bi.getLocation(being), gatheringLocation, being) +
                        STEPS_TO_GATHER_ENERGY + getTurn() <= CorporateMind.getInstance().getMaxTurn();
        }
    }

    private int getTurn() {
        return CorporateMind.getInstance().getTurn();
    }

    private int getStepToGather() {
        return CorporateMind.getInstance().getMaxTurn() - 22;
    }

    private int getPrepareToGatherStep() {
        return getStepToGather() - 10;
    }

    private class EnemyInfo {
        private Integer id;
        private Location location;

        EnemyInfo(Integer id, Location location) {
            this.id = id;
            this.location = location;
        }

        Integer getId() {
            return id;
        }

        Location getLocation() {
            return location;
        }
    }
}
